VC PlusPlus:Link Error LINK2019 未解決のシンボル…で参照されました。 対処方法

提供:yonewiki

VC PlusPlusに戻る

概要

 Visual Studioを使うプログラマなら、一度は目にするエラーです。他人が作ったプロジェクトとか複雑なプロジェクトになれば、その遭遇率は高くなります。

 これは、基本的にあなたの落ち度ではない事が多いのです。落胆しないで!自分で全てコーディングしたのにリンカエラーが出る場合は、自分の責任ですので、軽く自分の指に針を刺すなりして戒めていただければと思います。自分で自分を罰するというのは刺激的ですよ。アイスでも食べて頭を冷やして、甘やかしてもいいですけど。


 対処方法はいくつかのパターンに分かれています。解決が困難なモノもあります。他人のやることなのでね。簡単な対処方法が提供されていないとかあるよ。それでも乗り越えて行く力(チカラ)はあった方がいい。いかなる場合でも冷静に全ての整合性をとるということは、有能なプログラマである証拠だ。


 簡単ではないいくつかのパターンについて、ココで一緒に考えていきましょう。


知っていた方が良い知識

 1.切り分けという概念

 Libファイルを使うということは、よくわからない何かと結びつけることもあると思いますが、わからないなりに守っておくべきことがあります。考えすぎになってしまうこともありますが、32bit版でコンパイルするプログラムには32bit版の*.libファイルを、64bit版でコンパイルするプログラムでは64bit版の*.libファイルを用意するようにしましょう。ライブラリ側の作りによっては意識する必要がない場合もあります。


 そして、Debug版とRelease版もわけるべきです。例えば良く使われるマイクロソフトのC言語ランタイムライブラリでは、msvcrt.libという名前ですがデバッグ版はmsvcrtd.libという名前になっていたりします。Release版のライブラリ名のどこかにdを付け足すのが一般的です。おそらくDebugのdです。Debug版のライブラリって何?って感じですが、Debugという作業を開発環境で実施する場合は、作った関数やプログラムにDebug用のプログラムが一緒に生成されることで実現されています。読み込んだライブラリに対しても、1行づつプログラムを動かして追いかけるにはライブラリがDebug版として生成されたものである必要があります。違っていると必ずDebug版のライブラリを参照する仕組みになっている部分があるとリンクエラーになることもあります。Release版は容量が軽くて便利ですが、細かいDebugはできないです。ライブラリがReleaseであっても一行づつ追うことを諦める覚悟ならデバッグは出来ますが、Debug版でなければ動作しないこともあります。


 どこかから入手したオープンソースをビルドする場合、プロジェクトのプロパティ(コンパイルリンク処理のオプション引数)を見て、必要としているライブラリが記述されている場合があります。頑張って全部入手できなければ、そのオープンソースソフトウェアをビルドすることは出来ません。オープンソースだからといって、簡単にビルドできるとは限らない事を知っておきましょう。人のプログラムをビルドするだけのことでも、とんでもなく大変な作業になったりすることがあるという覚悟が必要です。手取り足取り、全ての手順をオープンソースプロジェクトの開発者が提供しているWebsiteになっているとは限りません。

 

知っていた方が良い小技

 1.Lib/Dllの中の関数を知る

 Lib/Dllファイルに目的の関数が格納されているかを確認する方法があるので、この方法を知っておいた方が良いでしょう。

 Libファイルには以下のようなコマンドを使います

dumpbin.exe /LINKERMEMBER (ライブラリファイル名or(完全パスor相対パス)\ライブラリファイル名)

 Dllファイルには以下のようなコマンドを使います

dumpbin.exe /EXPORTS (ダイナミックリンクライブラリファイル名or(完全パスor相対パス)\ダイナミックリンクライブラリファイル名)

 dumbinは、C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29333\bin\Hostx86\x86のようなパスの元にあるものが使われます。このようなコマンドを使う場合は、VisualStudioのメニュー[ツール]-[コマンドライン]-[開発者用PowerShell]から起動すると、パスがとおった状態のPowerShellのコマンドプロンプトが立ち上がります。このようにして起動した場合は単にdumpbinとするだけでコマンドが使えます。


 静的なライブラリ*.LibファイルはLINK処理の時点でエラーが出ますので、LINKERエラーは主にコチラの修正や確認作業が発生することでしょう。動的なライブラリ*.Dllはアプリを使っている時に発生するエラーになります。いずれにせよ、対応する関数がライブラリの中になかったら動きません。


 上記コマンドで、Lib・Dllファイルに格納される関数名が一覧できます。まずは読み込んでいるLibの中身に関数が本当に無いのか確認してみるのも良いでしょう。そうすれば、読み込み設定の問題ではなく、新たなライブラリの欠落やライブラリの中に必要な関数が格納されていない間違ったライブラリになっていることが原因だと考えることができます。

 

 2.Dll→LibのようなLib半自動生成の手順

 以下のようなコマンドを打つと生成できます。必ずしも出力できるわけではありません。dllを生成したプログラム側で__declspec(dllexport)というような宣言をしてあるものに限ります。

 defファイルの中身は自動で適切に生成されることもありますが、以下のような形式に整理しないといけない場合もあります。

以下はwintab32.dllっていうファイルから作ったdefファイルの抜粋です。

Dump of file wintab32.dll

File Type: DLL

  Section contains the following exports for Wintab32.dll

    00000000 characteristics
    59BBE69A time date stamp Fri Sep 15 23:41:30 2017
        0.00 version
          20 ordinal base
        1184 number of functions
          54 number of names

    ordinal hint RVA      name

         25    0 000079E1 Sync
         22    1 00006235 WTClose
         60    2 000089CC WTConfig
         81    3 000033C3 WTDataGet
         (省略)

というように吐き出された時のname欄が関数名でSyncとかWTCloseってのがソレにあたる。関数名には@が入ったりもします。

LIBRARY (ファイル名(拡張子を省く))
EXPORTS
        (関数名)
        (関数名)
        (関数名)
        (関数名)

 コマンドは

dumpbin.exe /EXPORT (DLLライブラリファイル名or(完全パスor相対パス)\DLLライブラリファイル名) > (ライブラリ名と同じ名前).def
lib /DEF:(ライブラリ名).def /MACHINE:X86 /out:(ライブラリ名).lib

 とします。Export可能な関数がなければエラーになります。あるといいね。

 

 3.パスが通っているか確認する

 きちんとLibを読み込んでいるつもりでも、環境変数のPathが通っていなくて、うまくいっていないことも多いです。そんなときは以下のようなコマンドでファイルが参照できるか試してみましょう。パワーシェルではなく、コマンドプロンプトが良いでしょう。

>where (ファイル名)
パス名\ファイル名

 パスが通っていてファイルがある場合はコマンドを確定した後に、そのファイルへの完全パスが表示されます。

 whereコマンドはオプションによっては特定のフォルダでの検索や、特定のフォルダ+サブフォルダでの検索といった検索もできますが、オプションを指定しない場合は、現在のディレクトリと環境変数のパスの中を検索してくれます。ファイル名にはワイルドカード * や ? が使えます。


 上記検索に加えてさらにもっと正確な検索をします。ライブラリディレクトリと追加のライブラリディレクトリというのをVisualStudioでは設定できるようになっていて、このパスの中に所定のlibファイルがあるかを検索します。ライブラリのディレクトリは、メニューの[プロジェクト]-[プロパティ]から[構成プロパティ]-[VC++ディレクトリ]にある[ライブラリディレクトリ]という設定のパスと[構成プロパティ]-[リンカ]-[追加のライブラリディレクト]に設定されているものです。$(xxx)とかっていうパスの場合は展開する必要がありますので、パス設定を選択して表示される[編集]を選択するとダイアログが起動します。評価された値というのが実際に検索されるパスです。変数では表現されていないので、扱い易いはずです。改行を \n を ; に変換するできるテキストエディタがあると、検索用のパス名を作りやすいです。

>where /R "(パス名)" (ファイル名)
パス名\ファイル名

(パス名)で複数を指定したい場合は ; セミコロンで区切って複数指定することが出来ます。


 パスが通っていない場合、パスを追加すると思いますが、なるべく相対パスを使うと良いでしょう。パスの基点はプロジェクト(*.vcxproj)ファイルの置かれているところからのパスになります。

 

対処方法

 

1.安全ではない古い関数が使われるパターン

 scanf, printf, strlen, strcmpといったC言語の基本とも言える関数を使ったときに発生します。昨今ではオーバーフローしないような、より安全な関数を使うのが定説になっていますが、勉強のため、ためしにgccとか古いVisual Studioで主に開発しているプログラマからプログラムをもらってきたときとかに発生します。古い関数自体は新しいVisual Studioでもコンパイルできるので、関数は新たにobjファイル化することが出来て問題なく使えます。これがlibファイル化された中で呼び出して、使われている場合です。この場合、他のlibファイルの中で古い関数が提供されている必要があり、リンカ設定をしておかないと駄目です。Visual Studioで最初から設定されているリンカ設定されているlibファイルからは、古い関数が消えています。つまり、参照先が無い。リンクエラーだよ。ってなります。なんで、こんな嫌がらせするのか?と思ってしまいますが、Micorsoftは昔ながらの関数群を標準ヘッダファイルでインライン定義(ヘッダファイルの中に関数の定義をして、ついでにプログラムの内容までヘッダファイルに記述する方法です)して内部で持たせるように変更したかったようです。libファイルによる外部ファイル参照を必要としない方法で扱いたかったんすかね。ナカナカ思い切った互換性を保たない変更です。


 プログラマを悩ませる変更をするなんて、アホかと馬鹿かと、愚痴ってもしょうがないです。こういう変更に敏感に対応できてこそプログラマたるプログラマなのだと突き進むしかないのです。


 メンドクサイ対応方法としては、入手したlibのソースファイルがあれば、それを入手してすべての関数をscanf_sのような最新のMSVC(MicrosoftVisualCpp)の標準libで提供されている関数を使ってlibファイルを再構築するのがいいのですが、scanf_sに変えて、受け取る文字列のサイズを調整するという作業をすべての基本関数に対して施術しなくてはなりません。それが割かし膨大なプロジェクトである場合、とてもじゃないけど、修正しきれないし、どんなに有能なプログラマでも何度も繰り返して同じことをやるだけにしても、どこかでミスタイプや、計算間違えでバグを起こす原因にもなります。それにそもそもソースが提供されている場合に限ります。


 そんな時は旧型対応のライブラリを使いましょう。安全とは言えない方法なので、大きな責任が伴うプログラムではあまりオススメしない方法です。使う人に危害が及んだりする可能性はゼロではないです。位置づけを理解して使ってもらえる範囲なら良いのかもしれません。どんなに頑張っても攻撃される可能性はあるので、世界的な流行りのプログラムでない限りは、深く考える必要はないのかもしれません。少数派のプログラムを狙っても凡人の個人情報ばかりで旨味は少ないですからね。攻撃のヤリガイがないと攻められません。割かしオープンソースでは適当になっているプログラムをよく見かけます。


  • 旧型のライブラリを使用して解決!
対処したいプロジェクトをアクティブにした状態で、メニューの[プロジェクト]-[プロパティ]を選択して表示されるダイアログで[構成プロパティ]-[リンカ]-[入力]の[追加の依存ファイル]に
legacy_stdio_definitions.lib
を追加します。なんだ、あるんじゃん。古い関数が使われたら、自動的にリンクしろよ。って思ったりもします。でも、本当に古い関数をそのまま使うかはプログラマ側で判断すべし!とMicrosoftは言っています。


 続きはまた気が向いたら書くので記事は増えていく予定。


 

2.C言語とC++言語の混在による呼び出し関数の互い違い

 Visual Studio C++を使っているつもりでも、C言語との互換性は高く、どちらを使っているか意識しなくなりがちですが、C言語で書かれたライブラリをC++の呼び出し規約で呼び出すと関数名が違うというエラーが発生します。呼び出しているライブラリがC言語で書かれた方式なのか?C++言語で書かれた方式なのか?を見極めるには、Dumpbinコマンドで、ライブラリの中の変数名を確認することで解決します。

dumpbin /LINKERMEMBER (確認したいライブラリの名前).lib


 ソースファイルがある場合は、ライブラリを作成するプロジェクトの中のプログラムファイルの拡張子が.cであったり、.cppであったりしています。C++でもCの規約に従ったライブラリを作成することができるので、拡張子だけでは判断しきれないかもしれません。


 で、どう違っているのか?なのですが、C++の場合はライブラリに書き出す関数の名称に名前マングリングという動作が追加されていて、zcallocのような関数はC言語方式だと_zcalloc、C++言語方式だと、?zcalloc@@YAPAXPAXII@Z、というような名前になります。なんじゃソレ?そのまえにこう言うエラーのときまず確認するべきはクラス関数の定義のときに名前空間をきちんと定義しないで記述したときとかにこんな感じのエラーが出ます。よく確認しましょう。ヘッダファイルではclass CxxProc{void CxxProc();};としたコンストラクタはプログラムコードを書く時にはvoid CxxProc::CxxProc(){…}と記述します。この先頭のCxxProc::をつけ忘れているとリンクエラーになります。関数が見つかりまへんってね。これは初歩のミスなのでこれ以外のもんだだとすると話は続きます。


 かなり、わけわからん@@とかYXPAとかどっから来たん?ってなると思います。そもそもマングリングってなんかヤラシイ名前で恥ずかしい(Embarrass)わ。とか言っている日本人のなんと多いことか。そうです。チコちゃんはマングリングが何なのかしっています。


 修飾~~~っ!


 装飾。英語のmanglingは日本語で修飾という意味です。マングリ返しとは関係ないし、ペロペロしたりもしません。チコちゃんは5歳なのにC++のマングリングまで知っているなんて、マングリ返しでもされたことをあるのかなぁ?「ませてはいませんて」そうなんです。C++では同一の関数名を名前空間を変更することによって定義できます。namespase ですね。だったり、オーバーロードという多重定義によって、引数が違うだけの関数名との切り分けも考慮した命名規則があります。命名規則は別の場所で解説するとして、元の名前がきちんとわかるようにわかりにくく修飾されています。元の名前は以下のようなundnameコマンドで確認できます。

>undname ?zcalloc@@YAPAXPAXII@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "?zcalloc@@YAPAXPAXII@Z"
is :- "void * __cdecl zcalloc(void *,unsigned int,unsigned int)"

 よく、dllを吐き出すプログラムでは、必ずextern "C"(囲われた部分はC言語の扱いで、という方法です。)で定義しましょうとか、言っている教則本やSiteがあったりしますが、名前マングリングの良さを分かっていないか、良さを教えるのを省いているということです。コンパイラによって、修飾規則が違うため、わけのからない技術で相互乗り入れも出来ず、dllの活用の幅が狭くなります。ようするに悪だとしているのだと思います。名前空間や、多重定義を利用するという点でメリットはあります。つまり作ったプログラムが同じコンパイラを使うもの同士だけが使えるdllだってあっていいわけです。なんなら、C言語の呼び出し規約で使いたいなら勝手に名前マングリングの結果を使って呼び出せばええじゃろ。って考え方もあります。これまでに紹介した方法で関数名は掌握できます。とは言ったモノの、Extern "C"を付けないDllは少ないかもしれない。


ちなみに

Sample.h

#ifdef __cplusplus
extern "C" {
#endif

void __cdecl zcalloc(void * vValue, unsigned int uiValue, unsigned int uiValue2);

#ifdef __cplusplus
}
#endif

 のように記述することでC++言語を使っている場合にC言語の関数形式に必ず変更するという定義が出来ます。まぁC言語に対応するとか、割かし難しい場合もあります。__cdeclキーワードは、dllに関数を書き出す時に必要になるもので、libファイルにはすべてが書き出されます。


 その場合は、参照するライブラリそのものをC++言語に作り替えるという方法があります。これは流石に面倒です。もう一つは、呼び出している関数を自前で再定義して使うという方法もあります。この場合、C言語で書かれたライブラリのプロジェクトの中にある特定プログラムファイルをSample.cというのをコピーして同じディレクトリにおいて、Sample.cとSample.cppが同じディレクトリに存在する状態になります。Sample.cやSample.cppに変更が発生した場合は両方を修正しなければならなくなります。


 そしてSample.hとSample.cppを自分が開発を進めているプロジェクトに取り込みます。インクルードパスの解決をしている場合は、Sample.hはプロジェクトに追加する必要はないかもしれません。なのでしっかりとSample.cppをプロジェクトに追加します。ヘッダファイルはSample.cpp内で#include "Sample.h"となっているなら、プロジェクトのプロパティのC/C++の項目の全般のところにある追加のインクルードファイルパスにSample.hがあるところを相対パスで指定します。必ずセットで移動する仕組みの関係になっていればです。一緒には動かさない。参照するライブラリ群は常に同じなら絶体パスで指定すると良いでしょう。であれば、本来ならばインクルードは#include <Sample.h>のように記述する方法をとるといいのかもしれません。元のファイルとの関係もあるので、あまり変更しない方がいいと思うので、"Sample.h"のような記述でインクルードするもの有りだとは思います。ちなみに<Sample.h>のようになっていれば、プロジェクトのプロパティのVCディレクトリという欄の追加のインクルードの方にパスを記述します。リンカエラーが何が参照できなかったかというヒントは表示されるので名前マングリング処理されたエラーが表示されても冷静に対処できるはずです。名前マングリングがややこしさを増殖させて感じてしまいがちですが、名前マングリングが起こることが分かっていれば、正確に何がどこに無くてエラーなのかっていうのは探知できると思います。


 C言語からC++言語に変更するには、ファイルの拡張子の変更だけで済む場合もありますが、関数の引数の型定義が、外出しになっている以下のように記述された関数

Sample.c

void zcalloc(vValue, uiValue, uiValue2)
void * vValue,
unsigned int uiValue,
unsigned int uiValue2
{
  
}

 を

Sample.cpp

void zcalloc(void * vValue, unsigned int uiValue, unsigned int uiValue2)
{
  
}

 と、こんな感じに修正する必要があります。メンドクサイ。


 このような不具合は同じプロジェクト内でlibを参照する場合でも同じです。プロジェクト内で半分引用したりした場合は、それ以外のコードを元々のライブラリを参照するようになっていて、半分引用してプロジェクト内では、Cの規約で呼び出されるように関係性が保たれていて引用したプロジェクトは問題なくビルド出来ます。しかし、そのプロジェクトに依存している中心的役割のプロジェクトからはC++の規約で引用したプロジェクトのlibを参照するようになっていれば、さらに引用元の関数を参照する際に、リンクエラーが発生します。さっきまでうまく行ってたのに依存してる関数だけリンクエラーとか意味わかんねぇっていう気分になります。実にややこしいことです。


 これが、C++とCの混合によるリンクエラーの仕組みです。要するに?なんとかして合わせろ!っていう事しか言えません。しかもC言語をC++言語に書き直したりする道を選んだときは、C言語独自に許された記法もあるので、割かしメンドクサイ変更をしつつ、あたらしく作ったCppファイルをメンテしないと駄目なこともあります。しっかりライブラリの設定をしたのにリンクエラーがでるときは、たぶん、こういう感じのリンクエラーが一番多いと思います。  

VC PlusPlusに戻る